/* Copyright 2009 Anthony Surma
This file is part of Java's Kademlia Extension (JKADX).

JKADX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

JKADX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with JKADX.  If not, see <http://www.gnu.org/licenses/>.
*/

package jkadx.dht;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Represents a contact triple: NodeID, IP Address, and Port.
 * Publicly immutable.
 *
 * @author Anthony Surma
 */
public final class Contact
{
    public    final static String            SEPERATOR      = "/";
    public    final static int               ADDRESS_OFFSET = 0; // 1st, 4 bytes
    public    final static int               PORT_OFFSET    = 4; // 2nd, 2 bytes
    public    final static int               ID_OFFSET      = 6; // 3rd, 20 bytes
    public    final static int               BYTE_LENGTH    = ID_OFFSET + UniqueId.NBITS / Byte.SIZE;
    private   final        int               hashCode;
              final        AtomicBoolean     isLocked       = new AtomicBoolean(false);
              final        byte[]            ipAddress;
              final        int[]             uniqueIdBits;
    public    final        UniqueId          uniqueId;
    public    final        int               port;
    private                InetSocketAddress socketAddress; // Lazy init.
    private                InetAddress       inetAddress; // Lazy init.

    public Contact(final UniqueId uniqueId, final byte[] ipAddress, final int port)
    {
	this.uniqueId      = uniqueId;
	this.ipAddress     = ipAddress;
	this.port          = port;
	this.hashCode      = uniqueId.hashCode;
        uniqueIdBits       = uniqueId.bits;
    }
        
    public Contact(final byte[] payload, int offset)
    {
        this.ipAddress     = new byte[] { payload[offset++], payload[offset++],
                             payload[offset++], payload[offset++] };
	this.port          = ((payload[offset++] & 0xFF) << 8) + (payload[offset++] & 0xFF);
	this.uniqueId      = new UniqueId(payload, offset);
        this.hashCode      = uniqueId.hashCode;
        uniqueIdBits       = uniqueId.bits;
    }

    public Contact(final byte[] payload, UniqueId uniqueId, int offset)
    {
        this.ipAddress     = new byte[] { payload[offset++], payload[offset++],
                             payload[offset++], payload[offset++] };
	this.port          = ((payload[offset++] & 0xFF) << 8) + (payload[offset] & 0xFF);
	this.uniqueId      = uniqueId;
        this.hashCode      = uniqueId.hashCode;
        uniqueIdBits       = uniqueId.bits;
    }

    @Testing Contact(final byte[] bytes)
    {
        this.ipAddress     = new byte[] { bytes[0], bytes[1], bytes[2], bytes[3] };
	this.port          = ((bytes[4] & 0xFF) << 8) + (bytes[5] & 0xFF);
	this.uniqueId      = new UniqueId(bytes, 6);
	this.hashCode      = uniqueId.hashCode;
        uniqueIdBits       = uniqueId.bits;
    }

    @Override
    public String toString()
    {
        StringBuilder result = new StringBuilder(183);

        try
        {
            result.append(uniqueId);
            result.append(SEPERATOR);
            result.append(getInetAddress().getHostAddress());
            result.append(SEPERATOR);
            result.append(port);
        } catch (UnknownHostException e) {}
        return result.toString();
    }

    @Override
    public boolean equals(final Object obj)
    {
	return this.uniqueId.matches(((Contact)obj).uniqueId);
    }

    @Override
    public int hashCode()
    {
        return hashCode;
    }

    // Lazy init, at least for now.
    public InetAddress getInetAddress() throws UnknownHostException
    {
        return (this.inetAddress != null) ?
               (this.inetAddress) :
               (this.inetAddress = InetAddress.getByAddress(ipAddress));
    }

    // Lazy init, at least for now.
    public InetSocketAddress getInetSocketAddress() throws UnknownHostException
    {
        return (this.socketAddress != null) ?
               (this.socketAddress) :
               (this.socketAddress  = (new InetSocketAddress(getInetAddress(), port)));
    }
        
    /**
     * Checks whether this contact's IP address and port matchesSocketAddress the IP address
     * and port of that contact.
     * <p>
     * Note: This method does not check nodeID's at all.  The equal method
     * does that.
     *
     * @param that The other contact to check against.
     * @return true if this contact's IP address and port matchesSocketAddress that
     * contact's IP address and port.  Returns false otherwise.
     */
    public boolean matchesSocketAddress(final Contact that) // XXX should switch ipaddresses and ports to byte arrays
    {
        return (this.port         == that.port        ) &&
               (this.ipAddress[3] == that.ipAddress[3]) &&
               (this.ipAddress[2] == that.ipAddress[2]) &&
               (this.ipAddress[1] == that.ipAddress[1]) &&
               (this.ipAddress[0] == that.ipAddress[0]) ;
    }
    
    public byte[] toByteArray() // each contact is 26 bytes
    {
        final byte[] result = new byte[6 + (UniqueId.NBITS / Byte.SIZE)];
        int    j      = 6;
        int intBit;
                
        result[0]  = ipAddress[0];
        result[1]  = ipAddress[1];
        result[2]  = ipAddress[2];
        result[3]  = ipAddress[3];
        result[4] |= port >>> 8;
        result[5] |= port;
                
        for (int i = 0; i < UniqueId.LENGTH; i++)
        {
            intBit = uniqueId.bits[i];

            result[j++] |= intBit >>> 24;
            result[j++] |= intBit >>> 16;
            result[j++] |= intBit >>> 8;
            result[j++] |= intBit;
        }
        return result;
    }

    public int insert(byte[] payload, int offset) // each contact is 26 payload
    {        
        int intBit;

        payload[offset++]  = ipAddress[0];
        payload[offset++]  = ipAddress[1];
        payload[offset++]  = ipAddress[2];
        payload[offset++]  = ipAddress[3];
        payload[offset++] |= port >>> 8;
        payload[offset++] |= port;

        for (int i = 0; i < UniqueId.LENGTH; i++)
        {
            intBit = uniqueId.bits[i];

            payload[offset++] |= intBit >>> 24;
            payload[offset++] |= intBit >>> 16;
            payload[offset++] |= intBit >>> 8;
            payload[offset++] |= intBit;
        }
        return offset;
    }

    public void insertAddress(final byte[] payload, int offset)
    {
        byte[] ipAddress = this.ipAddress;

        payload[offset++] = ipAddress[0];
        payload[offset++] = ipAddress[1];
        payload[offset++] = ipAddress[2];
        payload[offset]   = ipAddress[3];
    }

    public void insertPort(final byte[] payload, int offset)
    {
        payload[offset++] |= port >>> 8;
        payload[offset  ] |= port;
    }    
}
